﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;

namespace UnitBox {
	internal static class InterceptionQueue {

		#region class fields

		private static readonly IDictionary<MethodInfo, Queue<InterceptionBehaviour>> _interceptionQueues;
		private static readonly IList<MethodInfo> _methodsToStoreInvocationsFor;
		private static readonly IList<Expression> _methodCallsThatWereIntercepted;

		#endregion


		#region constructors

		static InterceptionQueue() {
			_interceptionQueues = new Dictionary<MethodInfo, Queue<InterceptionBehaviour>>();
			_methodsToStoreInvocationsFor = new List<MethodInfo>();
			_methodCallsThatWereIntercepted = new List<Expression>();
		}

		#endregion


		#region public methods

		/// <summary>
		/// Returns whether there are any interceptions pending for the given static method.
		/// </summary>
		/// <param name="method">the method which to check if there are pending interceptions for</param>
		/// <returns></returns>
		public static bool HasPendingInterceptionsTo( MethodInfo method ) {
			if( method == null )
				throw new ArgumentException( "method" );
			if( method == null || !method.IsStatic )
				throw new ArgumentException( "Method must be static." );

			return _interceptionQueues.ContainsKey( method );
		}


		/// <summary>
		/// Queues up an interception on the next invocation of the given static method 
		/// </summary>
		/// <param name="methodToIntercept"></param>
		/// <param name="interceptionBehaviour">TODO(design): what should this consist of?</param>
		public static void InterceptNextCallTo( MethodInfo methodToIntercept, InterceptionBehaviour interceptionBehaviour ) {
			if( methodToIntercept == null )
				throw new ArgumentException( "methodToIntercept" );
			if( interceptionBehaviour == null )
				throw new ArgumentException( "interceptionBehaviour" );
			if( !methodToIntercept.IsStatic )
				throw new ArgumentException( "Method must be static." );

			// create a queue of invocations to intercept if no interecptions are currently pending
			Queue<InterceptionBehaviour> interceptionQueueForMethod;
			if( /* no interception has yet been configured for the method */ !_interceptionQueues.ContainsKey( methodToIntercept ) ) {
				interceptionQueueForMethod = new Queue<InterceptionBehaviour>();
				_interceptionQueues[methodToIntercept] = interceptionQueueForMethod;
			} else {
				interceptionQueueForMethod = _interceptionQueues[methodToIntercept];
			}

			// queue up the interception
			interceptionQueueForMethod.Enqueue( interceptionBehaviour );
		}


		/// <summary>
		/// Retrieves the next interception for the given method.
		/// </summary>
		/// <param name="memberExpressionToIntercept">the method which to intercept</param>
		/// <returns></returns>
		public static InterceptionBehaviour GetNextPendingInterception( MemberExpression memberExpressionToIntercept ) {
			MethodInfo methodToIntercept = ((PropertyInfo)memberExpressionToIntercept.Member).GetGetMethod();

			// ensure the queue has a pending interception
			if( !_interceptionQueues.ContainsKey( methodToIntercept ))
				throw new InvalidOperationException( string.Format(
					"The method {0} has no pending interceptions.", methodToIntercept ));

			// retrieve the next interception
			Queue<InterceptionBehaviour> interceptionQueue = _interceptionQueues[methodToIntercept];
			InterceptionBehaviour interceptionBehaviour = interceptionQueue.Dequeue();

			// TODO(design): is this applicable?
			// store that the instance was swapped if configured to do so
			if( _methodsToStoreInvocationsFor.Contains( methodToIntercept )) {
				// TODO(design): what should we store about invocations?
				_methodCallsThatWereIntercepted.Add( memberExpressionToIntercept );
			}

			// remove queue if there are no more interceptions for the method
			if( interceptionQueue.Count == 0 )
				_interceptionQueues.Remove( methodToIntercept );

			return interceptionBehaviour;
		}


		/// <summary>
		/// Retrieves the next interception for the given method.
		/// </summary>
		/// <param name="methodCallExpressionToIntercept">the method which to intercept</param>
		/// <returns></returns>
		public static InterceptionBehaviour GetNextPendingInterception( MethodCallExpression methodCallExpressionToIntercept ) {
			MethodInfo methodToIntercept = methodCallExpressionToIntercept.Method;

			// ensure the queue has a pending interception
			if( !_interceptionQueues.ContainsKey( methodToIntercept ) )
				throw new InvalidOperationException( string.Format(
					"The method {0} has no pending interceptions.", methodToIntercept ) );

			// retrieve the next interception
			Queue<InterceptionBehaviour> interceptionQueue = _interceptionQueues[methodToIntercept];
			InterceptionBehaviour interceptionBehaviour = interceptionQueue.Dequeue();

			// TODO(design): is this applicable?
			// store that the instance was swapped if configured to do so
			if( _methodsToStoreInvocationsFor.Contains( methodToIntercept ) ) {
				// TODO(design): what should we store about invocations?
				_methodCallsThatWereIntercepted.Add( methodCallExpressionToIntercept );
			}

			// remove queue if there are no more interceptions for the method
			if( interceptionQueue.Count == 0 )
				_interceptionQueues.Remove( methodToIntercept );

			return interceptionBehaviour;
		}


		// TODO(design): clear the methods to remember invocations for
		/// <summary>
		/// Configures the queue to remember all future invocations that are going to be intercepted
		/// for the given method.
		/// </summary>
		/// <param name="methodCallExpression">the method for which to remember invocations for</param>
		public static void RememberInvocations( MethodCallExpression methodCallExpression ) {
			_methodsToStoreInvocationsFor.Add( methodCallExpression.Method );
		}


		/// <summary>
		/// Returns whether a call to the given method was intercepted.
		/// The values of parameters are ignored.
		/// </summary>
		/// <param name="methodThatShouldHaveBeenIntercepted"></param>
		/// <returns></returns>
		public static bool WasInterceptedWithAnyParameters( MethodCallExpression methodThatShouldHaveBeenIntercepted ) {
			return _methodCallsThatWereIntercepted
				.Cast<MethodCallExpression>()
				.Any( methodCallExpression =>
					methodCallExpression.Method == methodThatShouldHaveBeenIntercepted.Method );
		}


		/// <summary>
		/// Returns whether a call to the given method was intercepted.
		/// </summary>
		/// <param name="methodThatShouldHaveBeenIntercepted"></param>
		/// <returns></returns>
		public static bool WasInterceptedWithExactParameters( MethodCallExpression methodThatShouldHaveBeenIntercepted ) {
			throw new NotImplementedException( "TODO: Figure out how to compare parameter values" );

			return _methodCallsThatWereIntercepted.Contains( methodThatShouldHaveBeenIntercepted );
		}


		public static void Reset() {
			_interceptionQueues.Clear();
			_methodsToStoreInvocationsFor.Clear();
			_methodCallsThatWereIntercepted.Clear();
		}

		#endregion
		
	}
}
